{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Deep Research\n",
    "\n",
    "One of the classic cross-business Agentic use cases! This is huge."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/business.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00bfff;\">Commercial implications</h2>\n",
    "            <span style=\"color:#00bfff;\">A Deep Research agent is broadly applicable to any business area, and to your own day-to-day activities. You can make use of this yourself!\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I hope you find this lab useful! I would love to connect on LinkedIn: https://www.linkedin.com/in/kyranank/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Upgrades to Original System (Rough Notes)\n",
    "\n",
    "- When given a query, come up with 3 questions that will help answer the query\n",
    "    - Then come up with a search query to answer each question\n",
    "- Incorporate handoffs and using agents as tools\n",
    "    - Agents as Tools: 1 Google Search agent and 1 OpenAI Web Search agent. The research manager will decide if more queries are needed (and can call planner_agent again), or if need to run again.\n",
    "    - Agents as Tools: 1 question formulator agent and 1 search planner agent which get used by the research manager in sequence.\n",
    "    - Handoff the research to the writer agent\n",
    "    - Handoff the report to the editor agent\n",
    "\n",
    "PLAN\n",
    "- Create & test question_formulator_agent\n",
    "    - Given a query, formulate X research questions to help answer the query\n",
    "    - Test with a query\n",
    "    - Convert to .as_tool()\n",
    "- Adjust previous planner_agent & test\n",
    "    - Use the question_formulator_agent as a tool to generate questions\n",
    "    - Create 1 search query to answer each 1 of the questions\n",
    "    - Test with simulated questions\n",
    "    - Convert to .as_tool()\n",
    "- Create & test gemini_search_agent\n",
    "    - Someone has a version in the community contributions folder\n",
    "    - Take the list of queries and perform a search for each one\n",
    "    - Test with simulated query list\n",
    "    - Convert to .as_tool()\n",
    "- Adjust previous openai_search_agent & test\n",
    "    - Take the list of queries and perform a search for each one\n",
    "    - Test with simulated query list\n",
    "    - Convert to .as_tool()\n",
    "- Create an editor_agent\n",
    "    - Receives the report and edits it to a professional level\n",
    "    - Returns the same as writer_agent\n",
    "- Adjust the writer_agent\n",
    "    - Receives the research, writes a report, also returns a markdown report\n",
    "    - Handsoff to editor_agent\n",
    "- Create a research_manager_agent\n",
    "    - Given a query use the planner_agent to generate search queries to research the query\n",
    "    - Next use the gemini_search_agent and openai_search_agents to conduct the search queries. They should each be used once for every search query. That way you receive two different results to consider for each query for the final research report. If you believe more information is needed to address the query, you can use the planner_agent only one additional time followed by the gemini_search_agent and the gemini_search_agent to gather additional research. You will then handoff this research to the writer_agent to write a report."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from agents import Agent, WebSearchTool, trace, Runner, function_tool, handoff\n",
    "from agents.model_settings import ModelSettings\n",
    "from pydantic import BaseModel, Field\n",
    "from dotenv import load_dotenv\n",
    "import asyncio\n",
    "import os\n",
    "from sendgrid.helpers.mail import Mail, Email, To, Content\n",
    "from typing import Dict\n",
    "from IPython.display import display, Markdown\n",
    "from agents import OpenAIChatCompletionsModel\n",
    "from openai import AsyncOpenAI\n",
    "import requests\n",
    "from bs4 import BeautifulSoup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Question Formulator Agent\n",
    "- Create & test question_formulator_agent\n",
    "    - Given a query, formulate X research questions to help answer the query\n",
    "    - Test with a query\n",
    "    - Convert to .as_tool()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"You are a helpful research assistant. Given a query, formulate a set of research questions \\\n",
    "that when answered will help answer or address the query. Output 3 questions to research.\"\n",
    "\n",
    "class ResearchQuestionItem(BaseModel):\n",
    "    reason: str = Field(description=\"Your reasoning for why this question is important to address the query.\")\n",
    "    question: str = Field(description=\"The research question to use to conduct further research regarding the query.\")\n",
    "\n",
    "class ResearchQuestionPlan(BaseModel):\n",
    "    questions: list[ResearchQuestionItem] = Field(description=\"A list of research questions to research to best answer the query.\")\n",
    "\n",
    "question_formulator_agent = Agent(\n",
    "    name=\"Question Formulator Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=ResearchQuestionPlan,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Test - Question Formulator Agent\"):\n",
    "    result = await Runner.run(question_formulator_agent, message)\n",
    "\n",
    "print(result.final_output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Generate research questions\"\n",
    "\n",
    "tool_question_formulator = question_formulator_agent.as_tool(tool_name=\"question_formulator_agent\", tool_description=description)\n",
    "\n",
    "tools_for_planner_agent = [tool_question_formulator]\n",
    "\n",
    "tools_for_planner_agent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Planner Agent\n",
    "- Adjust previous planner_agent & test\n",
    "    - Use the question_formulator_agent as a tool to generate questions\n",
    "    - Create 1 search query to answer each 1 of the questions\n",
    "    - Test with simulated questions\n",
    "    - Convert to .as_tool()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"You are a helpful research assistant. Given a query you will use the question_formulator_agent as a tool to \\\n",
    "come up with a set of questions. Your role is to create 1 web search for each question to perform to best answer the query. \\\n",
    "Output the web search terms to query for, there should be exactly as many web search queries as questions you received.\"\n",
    "\n",
    "class WebSearchItem(BaseModel):\n",
    "    reason: str = Field(description=\"Your reasoning for why this search is important to the query and will answer the question.\")\n",
    "    query: str = Field(description=\"The search term to use for the web search.\")\n",
    "\n",
    "class WebSearchPlan(BaseModel):\n",
    "    searches: list[WebSearchItem] = Field(description=\"A list of web searches to perform to best answer the query.\")\n",
    "\n",
    "planner_agent = Agent(\n",
    "    name=\"Planner Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=WebSearchPlan,\n",
    "    tools=tools_for_planner_agent\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Test - Planner Agent\"):\n",
    "    result = await Runner.run(planner_agent, message)\n",
    "    print(result.final_output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Generate web search terms\"\n",
    "\n",
    "tool_planner_agent = planner_agent.as_tool(tool_name=\"planner_agent\", tool_description=description)\n",
    "tools_for_research_manager_agent = [tool_planner_agent]\n",
    "tools_for_research_manager_agent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Gemini Search Agent\n",
    "- Create & test gemini_search_agent\n",
    "    - Someone has a version in the community contributions folder\n",
    "    - Take the list of queries and perform a search for each one\n",
    "    - Test with simulated query list\n",
    "    - Convert to .as_tool()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "GEMINI_BASE_URL = \"https://generativelanguage.googleapis.com/v1beta/openai\"\n",
    "GOOGLE_API_KEY = os.getenv(\"GOOGLE_API_KEY\")\n",
    "GOOGLE_CUSTOM_SEARCH_API_KEY = os.getenv(\"GOOGLE_CUSTOM_SEARCH_API_KEY\")\n",
    "GOOGLE_CSE_ID = os.getenv(\"GOOGLE_CSE_ID\")\n",
    "\n",
    "gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=os.getenv('GOOGLE_API_KEY'))\n",
    "gemini_model = OpenAIChatCompletionsModel(model=\"gemini-2.0-flash\",openai_client=gemini_client)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Tool for Web Search - Google doesn't offer a remote WebSearchTool like OpenAI\n",
    "# Need to set up Google Programmable Search / Custom Search API and get a key + search engine ID\n",
    "\n",
    "@function_tool\n",
    "async def google_web_search(query: str) -> str:\n",
    "    \"\"\"\n",
    "    Performs a Google Search and returns a string of concatenated result snippets.\n",
    "    \"\"\"\n",
    "    endpoint = \"https://www.googleapis.com/customsearch/v1\"\n",
    "    params = {\n",
    "        \"key\": GOOGLE_CUSTOM_SEARCH_API_KEY,\n",
    "        \"cx\": GOOGLE_CSE_ID,\n",
    "        \"q\": query,\n",
    "        \"num\": 5,  # Number of results to fetch\n",
    "    }\n",
    "\n",
    "    response = requests.get(endpoint, params=params)\n",
    "    if response.status_code != 200:\n",
    "        raise RuntimeError(f\"Google Search API error {response.status_code}: {response.text}\")\n",
    "\n",
    "    data = response.json()\n",
    "    items = data.get(\"items\", [])\n",
    "\n",
    "    if not items:\n",
    "        return \"No results found.\"\n",
    "\n",
    "    # Combine title and snippet for each result\n",
    "    result_texts = []\n",
    "    for item in items:\n",
    "        title = item.get(\"title\", \"\").strip()\n",
    "        snippet = item.get(\"snippet\", \"\").strip()\n",
    "        link = item.get(\"link\", \"\").strip()\n",
    "        result_texts.append(f\"{title}\\n{snippet}\\n{link}\")\n",
    "\n",
    "    summary = \"\\n\\n\".join(result_texts)\n",
    "    return summary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"You are a research assistant. Given a web search term, you search the web using the google_web_search tool \\\n",
    "for that term and produce a concise summary of the results. The summary must 5-8 paragraphs and between 500-1000 words \\\n",
    "words. Capture the main points. Write succintly, add evidence/facts, no need to have complete sentences or good \\\n",
    "grammar. This will be consumed by someone synthesizing a report, so it's vital you capture the \\\n",
    "essence and ignore any fluff. Do not include any additional commentary other than the summary itself.\"\n",
    "\n",
    "gemini_search_agent = Agent(\n",
    "    name=\"Gemini Search Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    tools=[google_web_search],\n",
    "    model=gemini_model,\n",
    "    model_settings=ModelSettings(tool_choice=\"required\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"notable AI agent frameworks introduced or updated in 2025\"\n",
    "\n",
    "with trace(\"Test - Gemini Search Agent\"):\n",
    "    result = await Runner.run(gemini_search_agent, message)\n",
    "\n",
    "display(Markdown(result.final_output))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Search the web using Google Search\"\n",
    "\n",
    "tool_google_search = gemini_search_agent.as_tool(tool_name=\"gemini_search_agent\", tool_description=description)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools_for_research_manager_agent.append(tool_google_search)\n",
    "tools_for_research_manager_agent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### OpenAI Search Agent\n",
    "- Adjust previous openai_search_agent & test\n",
    "    - Take the list of queries and perform a search for each one\n",
    "    - Test with simulated query list\n",
    "    - Convert to .as_tool()\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"You are a research assistant. Given a web search term, you search the web \\\n",
    "for that term and produce a concise summary of the results. The summary must 5-8 paragraphs and between 500-1000 words \\\n",
    "words. Capture the main points. Write succintly, add evidence/facts, no need to have complete sentences or good \\\n",
    "grammar. This will be consumed by someone synthesizing a report, so it's vital you capture the \\\n",
    "essence and ignore any fluff. Do not include any additional commentary other than the summary itself.\"\n",
    "\n",
    "openai_search_agent = Agent(\n",
    "    name=\"Search Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    tools=[WebSearchTool(search_context_size=\"low\")],\n",
    "    model=\"gpt-4o-mini\",\n",
    "    model_settings=ModelSettings(tool_choice=\"required\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"notable AI agent frameworks introduced or updated in 2025\"\n",
    "\n",
    "with trace(\"Search\"):\n",
    "    result = await Runner.run(openai_search_agent, message)\n",
    "\n",
    "display(Markdown(result.final_output))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Search the web using OpenAI's WebSearchTool\"\n",
    "\n",
    "tool_openai_search = openai_search_agent.as_tool(tool_name=\"openai_search_agent\", tool_description=description)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools_for_research_manager_agent.append(tool_openai_search)\n",
    "tools_for_research_manager_agent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Editor Agent\n",
    "- Create an editor_agent\n",
    "    - Receives the report and edits it to a professional level\n",
    "    - Returns the same as writer_agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ReportData(BaseModel):\n",
    "    short_summary: str = Field(description=\"A short 2-3 sentence summary of the findings.\")\n",
    "    markdown_report: str = Field(description=\"The final report\")\n",
    "    follow_up_questions: list[str] = Field(description=\"Suggested topics to research further\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = (\n",
    "    \"You are a senior editor tasked with editing and revising a cohesive report for a research query. \"\n",
    "    \"You will be provided with the report data including a short summary, full markdown_report, and some follow_up_questions\\n\"\n",
    "    \"You should first review the report, analyze the structure, correct grammar, spelling, sentence structure, etc. for profressionalism\"\n",
    "    \"Then, create a revised report using the original report as a basis and return that as your final output.\\n\"\n",
    "    \"The final output should be in markdown format, and it should be lengthy and detailed with FULL SENTENCES.\"\n",
    "    \"This is a research report, aim for 10-15 pages of content, AT LEAST 2000 words, maximum of 5000 words.\"\n",
    ")\n",
    "\n",
    "editor_agent = Agent(\n",
    "    name=\"Editor Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=ReportData\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Writer Agent\n",
    "- Adjust the writer_agent\n",
    "    - Receives the research, writes a report, also returns a markdown report\n",
    "    - Handsoff to editor_agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = (\n",
    "    \"You are a senior researcher tasked with writing a cohesive report for a research query. \"\n",
    "    \"You will be provided with the original query, and some initial research done by a research assistant.\\n\"\n",
    "    \"You should first come up with an outline for the report that describes the structure and \"\n",
    "    \"flow of the report. Then, generate the report using the research as a basis and return that as your final output.\\n\"\n",
    "    \"The final output should be in markdown format, and it should be lengthy and detailed. Aim \"\n",
    "    \"for 10-15 pages of content, at least 2000 words.\"\n",
    "    \"Once you have gathered all research results, hand off the complete set of research materials to the `editor_agent`.\\n\"\n",
    "    \"The `writer_agent` will be responsible for synthesizing the information into a final research report.\"\n",
    ")\n",
    "\n",
    "writer_agent = Agent(\n",
    "    name=\"Writer Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=ReportData,\n",
    "    handoffs=[handoff(editor_agent)]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Research Manager Agent\n",
    "- Create a research_manager_agent\n",
    "    - Given a query use the planner_agent to generate search queries to research the query\n",
    "    - Next use the gemini_search_agent and openai_search_agents to conduct the search queries. They should each be used once for every search query. That way you receive two different results to consider for each query for the final research report. If you believe more information is needed to address the query, you can use the planner_agent only one additional time followed by the gemini_search_agent and the gemini_search_agent to gather additional research. You will then handoff this research to the writer_agent to write a report."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"\"\"\n",
    "You are a Research Manager Agent responsible for coordinating a multi-step research process to produce high-quality research reports.\n",
    "\n",
    "Follow this workflow precisely:\n",
    "\n",
    "1. **Planning Phase**\n",
    "   - When given a research query, always start by using the `planner_agent`.\n",
    "   - Use the `planner_agent` to generate a set of structured search queries. The planner will return these queries as a Pydantic model.\n",
    "\n",
    "2. **Initial Research Phase**\n",
    "   - For each search query provided by the planner, conduct research using both of the following agents:\n",
    "     - `gemini_search_agent`\n",
    "     - `openai_search_agent`\n",
    "   - You must use each of these search agents exactly once per search query. This ensures you gather two different perspectives for every query.\n",
    "\n",
    "3. **Supplemental Research (if necessary)**\n",
    "   - After reviewing the initial results, if you determine that additional information is required to adequately address the original research query, you may proceed to collect more research.\n",
    "   - To do this, you may call the `planner_agent` **only one additional time** to generate more search queries.\n",
    "   - For each additional search query, again use both:\n",
    "     - `gemini_search_agent`\n",
    "     - `openai_search_agent`\n",
    "   - Do not exceed this single additional round of planning and research.\n",
    "\n",
    "4. **Report Handoff**\n",
    "   - Once you have gathered all research results, hand off the complete set of research materials to the `writer_agent`.\n",
    "   - The `writer_agent` will be responsible for synthesizing the information into a final research report.\n",
    "\n",
    "Important Guidelines:\n",
    "- Always perform the research workflow in the sequence described: planner → search agents → (optional, only allowed to call 1 additional time) planner → search agents → writer.\n",
    "- Do not skip any steps or call the search agents before the planner.\n",
    "- DO NOT call the planner more than two (2) times TOTAL per query. You should only call it a maximum of 1 initial time and 1 additional time, for a total of 2 times.\n",
    "- Ensure all gathered results are organized clearly before passing them to the writer agent.\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "research_manager_agent = Agent(\n",
    "    name=\"Research Manager Agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    tools=tools_for_research_manager_agent,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    model_settings=ModelSettings(tool_choice=\"required\"),\n",
    "    handoffs=[handoff(writer_agent)]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# RUN AGENTIC SYSTEM HERE!\n",
    "\n",
    "message = \"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Search\"):\n",
    "    final_result = await Runner.run(research_manager_agent, message)\n",
    "    print(\"Report Complete!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "display(Markdown(final_result.final_output.short_summary))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "display(Markdown(final_result.final_output.markdown_report))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Trace\n",
    "\n",
    "https://platform.openai.com/traces"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from agents.extensions.visualization import draw_graph\n",
    "draw_graph(research_manager_agent)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from agents.extensions.visualization import draw_graph\n",
    "draw_graph(writer_agent)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from agents.extensions.visualization import draw_graph\n",
    "draw_graph(editor_agent)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
